package com.i360day.redis.aspect;

import com.i360day.redis.annotation.RedisCache;
import com.i360day.redis.annotation.RedisEvict;
import com.i360day.redis.configuration.manager.RedisCacheManageTemplate;
import com.i360day.redis.lock.RedisLock;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

@Aspect
public class RedisCacheAspect {
    private final static Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class);

    private final String reids_cache_lock = "redis_cache_lock_";

    @Autowired
    private RedisCacheManageTemplate redisCacheManageTemplate;

    /**
     * 定义切入点，使用了 @RedisCache 的方法
     */
    @Pointcut("@annotation(com.i360day.redis.annotation.RedisCache)")
    public void redisCachePoint() {
    }

    @Pointcut("@annotation(com.i360day.redis.annotation.RedisEvict)")
    public void redisEvictPoint() {
    }


    @After("redisEvictPoint()")
    public void evict(JoinPoint point) {
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        RedisEvict redisEvict = method.getAnnotation(RedisEvict.class);
        // 获取RedisCache注解
        String fieldKey = parseKey(redisEvict.fieldKey(), method, point.getArgs());
        String rk = redisEvict.key() + ":" + fieldKey;
        logger.debug("<======切面清除rediskey:{} ======>" + rk);
        redisCacheManageTemplate.delete(rk);
    }

    /**
     * 环绕通知，方法拦截器
     */
    @Around("redisCachePoint()")
    public Object WriteReadFromRedis(ProceedingJoinPoint point) throws Throwable {
        ValueOperations<String, Object> cacheValueOperations = redisCacheManageTemplate.opsForValue();
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        // 获取RedisCache注解
        RedisCache redisCache = method.getAnnotation(RedisCache.class);
        if (redisCache == null) return point.proceed();

        if (redisCache.read()) {
            try {
                logger.debug("<======method:{} 进入 redisCache 切面 ======>", method.getName());
                String fieldKey = parseKey(redisCache.fieldKey(), method, point.getArgs());
                String rk = redisCache.key() + ":" + fieldKey;
                AtomicReference<Object> atomicReference = new AtomicReference<>(cacheValueOperations.get(rk));
                Object objValue = atomicReference.get();
                //双向同步锁
                if (objValue == null && redisCache.sync()) {
                    synchronized (this.reids_cache_lock) {
                        //单应用穿透后
                        atomicReference.set(cacheValueOperations.get(rk));
                        if (atomicReference.get() != null) return atomicReference.get();
                        //一旦访问过高，阻塞后需要降级处理
                        return RedisLock.instance(reids_cache_lock + rk, 60 * 60).call(() -> {
                            //多应用穿透后
                            atomicReference.set(cacheValueOperations.get(rk));
                            if (atomicReference.get() != null) return atomicReference.get();
                            // Redis 中不存在，则从数据库中查找，并保存到 Redis
                            Object returnObj = point.proceed();
                            if (returnObj != null) {
                                if (redisCache.expired() > 0) {
                                    redisCacheManageTemplate.setIfPresent(rk, returnObj, redisCache.expired(), TimeUnit.SECONDS);
                                } else {
                                    redisCacheManageTemplate.setIfPresent(rk, returnObj);
                                }
                            }
                            return returnObj;
                        });
                    }
                }
                //串行
                else if (objValue == null) {
                    // Redis 中不存在，则从数据库中查找，并保存到 Redis
                    Object returnObj = point.proceed();
                    if (returnObj != null) {
                        if (redisCache.expired() > 0) {
                            redisCacheManageTemplate.setIfPresent(rk, returnObj, redisCache.expired(), TimeUnit.SECONDS);
                        } else {
                            redisCacheManageTemplate.setIfPresent(rk, returnObj);
                        }
                    }
                    return returnObj;
                } else {
                    return objValue;
                }
            } catch (Throwable throwable) {
                logger.error("RedisCache 执行异常: {}", throwable);
                throw throwable;
            }
        }
        return point.proceed();
    }

    /**
     * 获取缓存的key
     * key 定义在注解上，支持SPEL表达式
     *
     * @return
     */
    private String parseKey(String key, Method method, Object[] args) {
        // 获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        // 使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        // SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(key).getValue(context, String.class);
    }
}
